Skip to content

bundler: make sourcemap sources relative to the map file, not outdir - #36601

Closed
robobun wants to merge 4 commits into
mainfrom
farm/55ab8b18/sourcemap-sources-relative-to-map-dir
Closed

robobun wants to merge 4 commits into
mainfrom
farm/55ab8b18/sourcemap-sources-relative-to-map-dir

Conversation

@robobun

@robobun robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3332.

What

When Bun.build emits chunks into subdirectories of outdir (multiple entrypoints under a root, or a [dir] naming template), the sources array in each .js.map was computed relative to outdir instead of the directory containing the .map file. Per the source map spec, sources entries are resolved relative to the map file (or sourceRoot), so debuggers following them ended up at nonexistent paths like out/repos/livex/web/xxx/....

Repro

await Bun.build({
  root: "/repro/xxx",
  entrypoints: [
    "/repro/xxx/repos/livex/web/src/index.ts",
    "/repro/xxx/repos/livex/server/src/bundler/foo.ts",
  ],
  outdir: "/repro/out",
  sourcemap: "external",
});

Before: out/repos/livex/web/src/index.js.map contains "sources": ["../xxx/node_modules/tiny-dep/index.js", ...], which resolves (from the map's directory) to out/repos/livex/web/xxx/... and does not exist.

After: "sources": ["../../../../../xxx/node_modules/tiny-dep/index.js", ...], which resolves to /repro/xxx/node_modules/tiny-dep/index.js.

Cause

post_process_{js,css}_chunk passed resolver.opts.output_dir as chunk_abs_dir to generate_source_map_for_chunk, so every sources entry was relative(outdir, source) regardless of where in outdir the chunk actually lands.

Fix

Compute chunk_abs_dir = output_dir + dirname(chunk.template) at post-process time, matching esbuild's fs.Dir(fs.Join(AbsOutputDir, TemplateToString(chunk.finalTemplate))). chunk.final_rel_path is not yet populated (its content hash depends on post-process output), but the hash formatter never emits a path separator, so substituting a dummy value in PathTemplate::rel_dir yields the correct directory depth even when [hash] appears in a directory segment of the naming template.

When rel_dir is empty or . (flat outdir), or output_dir is empty (no outdir), the previous behavior is preserved.

Verification

New test/regression/issue/03332.test.ts builds a monorepo-style layout and asserts every sources[i] resolves from the map file's own directory for both sourcemap: "external" and sourcemap: "inline". Fails on main with source "../xxx/node_modules/tiny-dep/index.js" -> .../out/repos/livex/web/xxx/node_modules/tiny-dep/index.js not existing; passes with this change. Existing snapshotSourceMap tests in bundler_edgecase.test.ts, bundler_npm.test.ts, and internal-sourcemap-roundtrip.test.ts are unaffected (all use a flat outdir).


[review] gate passed · iteration 0 · 5 files touched

fails on main (without fix)
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/regression/issue/03332.test.ts"
bun test v1.4.0 (7fdb4fa93)

test/regression/issue/03332.test.ts:
59 |       for (const src of json.sources as string[]) {
60 |         const resolved = path.resolve(mapDir, src);
61 |         // Every source entry must resolve to a real file from the map's own
62 |         // directory. Before the fix these resolved under out/repos/.../xxx/...
63 |         // which does not exist.
64 |         expect(existsSync(resolved), `source ${JSON.stringify(src)} in ${map.path} -> ${resolved}`).toBe(true);
                                                                                                         ^
error: source "../xxx/node_modules/tiny-dep/index.js" in /tmp/issue-3332_G71gtG/out/repos/livex/web/src/index.js.map -> /tmp/issue-3332_G71gtG/out/repos/livex/web/xxx/node_modules/tiny-dep/index.js

Expected: true
Received: false

      at <anonymous> (/workspace/bun/test/regression/issue/03332.test.ts:64:101)
(fail) issue 3332: sourcemap sources are relative to the map file > nested outdir layout (mo
... (truncated)

release without fix: 3 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/regression/issue/03332.test.ts:
59 |       for (const src of json.sources as string[]) {
60 |         const resolved = path.resolve(mapDir, src);
61 |         // Every source entry must resolve to a real file from the map's own
62 |         // directory. Before the fix these resolved under out/repos/.../xxx/...
63 |         // which does not exist.
64 |         expect(existsSync(resolved), `source ${JSON.stringify(src)} in ${map.path} -> ${resolved}`).toBe(true);
                                                                                                         ^
error: source "../xxx/node_modules/tiny-dep/index.js" in /tmp/issue-3332_6YbtUX/out/repos/livex/web/src/index.js.map - /tmp/issue-3332_6YbtUX/out/repos/livex/web/xxx/node_modules/tiny-dep/index.js

Expected: true
Received: false

      at <anonymous> (/workspace/bun/test/regression/issue/03332.test.ts:64:101)
(fail) issue 3332: sourcemap sources are relative to the map file > nested outdir layout (monorepo-style root) [57.05ms]
(pass) issue 3332: sourcemap sources are relative to the map file > flat outdir (single entrypoint in outdir root) still resolves [32.
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/regression/issue/03332.test.ts"
bun test v1.4.0 (7fdb4fa93)

test/regression/issue/03332.test.ts:
(pass) issue 3332: sourcemap sources are relative to the map file > nested outdir layout (monorepo-style root) [280.31ms]
(pass) issue 3332: sourcemap sources are relative to the map file > flat outdir (single entrypoint in outdir root) still resolves [330.53ms]
(pass) issue 3332: sourcemap sources are relative to the map file > inline sourcemap in a nested chunk resolves from the map file [110.55ms]
(pass) issue 3332: sourcemap sources are relative to the map file > relative outdir with nested chunks [1580.97ms]

 4 pass
 0 fail
 30 expect() calls
Ran 4 tests across 1 file. [4.71s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1142ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli)
�[1m�[92m   Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output)
�[1m�[92m   Compiling�[0m bun_clap
... (truncated)
diff hotspot
src/bundler/LinkerContext.rs                      |  22 +++
 src/bundler/linker_context/postProcessCSSChunk.rs |   8 +-
 src/bundler/linker_context/postProcessJSChunk.rs  |   7 +-
 src/bundler/options.rs                            |  20 +++
 test/regression/issue/03332.test.ts               | 178 ++++++++++++++++++++++
 5 files changed, 232 insertions(+), 3 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                               reads  edits  tests
src/bundler/LinkerContext.rs                           7      5      0
src/bundler/linker_context/postProcessCSSChunk.rs      2      1      0
src/bundler/linker_context/postProcessJSChunk.rs       2      1      0
src/bundler/options.rs                                 5      2      0
test/regression/issue/03332.test.ts                    1      5      0

When a build emits chunks into subdirectories of outdir (multiple
entrypoints with a root, or a [dir] naming template), the sourcemap
sources array was computed relative to the outdir instead of the
directory containing the .map file. Per the sourcemap spec (and
esbuild), sources entries are resolved relative to the map file
itself, so debuggers following them ended up at nonexistent
out/<subdir>/<outdir-relative> paths.

Compute chunk_abs_dir as output_dir + dirname(template) at
post-process time. The content hash is not yet known there, but the
hash formatter never emits a path separator, so a dummy value gives
the correct directory depth.

Fixes #3332
@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cd892c45-f92e-462d-b679-de0181913fac

📥 Commits

Reviewing files that changed from the base of the PR and between df49a6e and 7fdb4fa.

📒 Files selected for processing (5)
  • src/bundler/LinkerContext.rs
  • src/bundler/linker_context/postProcessCSSChunk.rs
  • src/bundler/linker_context/postProcessJSChunk.rs
  • src/bundler/options.rs
  • test/regression/issue/03332.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Reproduced on canary, fixed, PR #36601.

MAP: out/repos/livex/web/src/index.js.map
  src: ../../../../../xxx/node_modules/tiny-dep/index.js    viaMapDir: OK
  src: ../../../../../xxx/repos/livex/store/src/util.ts     viaMapDir: OK
  src: ../../../../../xxx/repos/livex/web/src/index.ts      viaMapDir: OK

@github-actions github-actions Bot added the claude label Aug 1, 2026
Comment thread src/bundler/LinkerContext.rs Resolved
Comment thread src/bundler/options.rs Resolved
Comment thread src/bundler/LinkerContext.rs Resolved
Comment thread src/bundler/options.rs Resolved
@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:26 PM PT - Jul 31st, 2026

✅ @robobun, your commit 7fdb4fa938e13ec5163a69017ad50940b30c844b passed in Build #86566! 🎉


🧪   To try this PR locally:

bunx bun-pr 36601

That installs a local version of the PR into your bun-36601 executable, so you can run:

bun-36601 --bun

Comment thread src/bundler/LinkerContext.rs Resolved
Comment thread test/regression/issue/03332.test.ts Resolved
Comment thread test/regression/issue/03332.test.ts Resolved
join_abs_string_buf requires an absolute cwd; Bun.build stores a
relative outdir verbatim. Anchor the join at FileSystem top_level_dir
so a relative outdir is resolved the same way relative_platform_buf
already does for the flat case. Adds a subprocess test that builds
with outdir: 'dist' and asserts sources resolve from the map dir.

Also guard the flat/inline tests against vacuously passing on an
empty sources array.
Comment thread src/bundler/LinkerContext.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — both prior findings addressed.

What was reviewed:

  • Verified 7fdb4fa's relative-outdir fix: join_abs_string_buf resets its base when a part is absolute (resolve_path.rs:1712), so anchoring at top_level_dir is correct for both absolute and relative output_dir; new subprocess test covers it.
  • Verified the rel_dir dummy-hash invariant: truncated_hash32 alphabet is 0-9a-z only, no path separators.
  • Tests 2/3 now guard sources.length >= 2 before iterating.
  • The CSS-chunk call site remains untested (prior nit), but it's a mechanical mirror of the tested JS site and the shared helper is exercised.
Extended reasoning...

Overview

This PR fixes #3332: sourcemap sources entries were computed relative to outdir rather than the .map file's own directory, breaking debugger source lookup when chunks land in subdirectories. The fix introduces LinkerContext::source_map_chunk_abs_dir (joins output_dir with the chunk template's directory portion) and PathTemplate::rel_dir (expands the template with a dummy hash and takes dirname), then routes both post_process_js_chunk and post_process_css_chunk through it. Five files touched: two call sites, one new helper each in LinkerContext.rs and options.rs, and a new regression test file with four cases.

Prior review follow-up

I previously flagged (1) a 🔴 regression where join_abs_string_buf was called with a possibly-relative output_dir as its cwd, corrupting the base on POSIX and tripping a debug_assert on Windows, and (2) a 🟡 vacuous-loop pattern in tests 2/3. Both are addressed in the current diff: 7fdb4fa anchors the join at FileSystem::get().top_level_dir (always absolute) with [output_dir, rel_dir] as parts — I traced _join_abs_string_buf and confirmed an absolute part resets cwd (resolve_path.rs:1707-1722), so absolute-outdir behavior is unchanged. A subprocess test with outdir: "dist" covers the relative case. Tests 2/3 now assert sources.length >= 2 before the loop. The prior 🟡 CSS-test-coverage suggestion was not acted on; I consider it non-blocking since the CSS call site is a four-line copy of the tested JS call site and any bug in the shared helper would be caught by the JS tests.

Security risks

None. This affects only the relative-path strings written into emitted .js.map files. No untrusted input parsing, no filesystem writes beyond what Bun.build already does, no auth/crypto surface.

Level of scrutiny

Medium. Bundler sourcemap output is user-visible and production-relevant, but the change is narrow: the flat-outdir / no-outdir early-return preserves prior behavior byte-for-byte, and the new path only changes the base directory fed to the existing relative_alloc. I additionally verified the rel_dir dummy-hash trick is sound — truncated_hash32_bytes (fmt.rs:3466) uses a base32 alphabet with no / or \\, so the directory depth is invariant under the eventual hash.

Other factors

Four tests cover nested-external, flat-external, nested-inline, and relative-outdir. The PR description states existing snapshotSourceMap tests are unaffected (all flat outdir → early-return path). path_buffer_pool::get() returns a Drop guard, so no buffer leak. The author was responsive to review feedback, and the bug hunting system found nothing new on the current revision.

@github-actions

Copy link
Copy Markdown
Contributor

Closing this PR because it has been inactive for more than 90 days.

@robobun

robobun commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator Author

Continued in #47548. It keeps the rule of this PR (the outdir plus the directory of the naming template, with a stand-in hash) and moves it into the source map writer. It also keeps the entries of executables and of standalone HTML with an inline map, which this PR changed by mistake.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sourcemap source root not clear

2 participants